﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Xml;
using System.Xml.Schema;
using Borg.Properties;

namespace Borg
{
    internal sealed class XmlSettingProvider : SettingProvider
    {
        private const string SchemaFile = "Settings.XmlSchema.xsd";
        private const string NamespaceUri = "http://borg.net/SettingsSchema/";
        private const string SettingsSubDirectory = "Settings";
        private const string SettingsFileExtension = ".Settings.xml";
        private const int ReloadSettingsDelay = 20;

        private volatile Thread _updateThread;

        private readonly XmlSchema _xmlSchema;

        private string _settingsDirectory;
        private Dictionary<string, Plugin> _loadedSettings;

        private HashSet<string> _isSavingSet;
        private FileSystemWatcher _fileSystemWatcher;

        public XmlSchema XmlSchema
        {
            get { return _xmlSchema; }
        }

        public XmlSettingProvider(Runtime runtime, string path)
            : base(runtime, path)
        {
            using (var manifestResourceStream = Assembly.GetExecutingAssembly().
                GetManifestResourceStream(typeof(XmlSettingProvider), SchemaFile))
            {
                Debug.Assert(manifestResourceStream != null);
                _xmlSchema = XmlSchema.Read(manifestResourceStream, null);
            }
        }

        protected internal override void Init()
        {
            var application = Runtime.Application;

            _settingsDirectory =
                string.IsNullOrEmpty(path)
                    ? Path.Combine(Environment.GetFolderPath(
                        Environment.SpecialFolder.ApplicationData),
                                   application.Company,
                                   application.Name,
                                   SettingsSubDirectory)
                    : path;
            
            _loadedSettings = new Dictionary<string, Plugin>();
            _isSavingSet = new HashSet<string>();
            
            if (Directory.Exists(_settingsDirectory))
                EnableFileSystemWatcher();
            
            Runtime.ShutdownStarted += DisableFileSystemWatcher;
        }

        protected internal override OrderedDictionary<ExtensionType,
            SettingDictionary> LoadSettings(Plugin plugin)
        {
            var xmlDocument = LoadSettings(plugin, GetSettingsPath(plugin), true);

            return
                xmlDocument != null
                    ? ReadPluginSettings(plugin, xmlDocument)
                    : null;
        }

        protected internal override void SaveSettings(Plugin plugin,
            OrderedDictionary<ExtensionType, SettingDictionary> settings)
        {
            var xmlDocument = WritePluginSettings(settings, plugin);
            SaveSettings(plugin, xmlDocument, GetSettingsPath(plugin));
        }

        private XmlDocument LoadSettings(Plugin plugin, string xmlPath, bool logErrors)
        {
            _loadedSettings[xmlPath] = plugin;
            
            if (!File.Exists(xmlPath))
                return null;

            var xmlDocument = new XmlDocument();

            var settings = new XmlReaderSettings
            {
                ValidationType = ValidationType.Schema
            };

            settings.ValidationEventHandler += ValidationEventHandler;
            settings.Schemas.Add(_xmlSchema);
            
            try
            {
                var timeout = 0;

                while (true)
                {
                    try
                    {
                        using (var reader = XmlReader.Create(xmlPath, settings))
                        {
                            xmlDocument.Load(reader);
                            break;
                        }
                    }
                    catch (Exception)
                    {
                        ++timeout;

                        if (timeout == ReloadSettingsDelay)
                            throw;

                        Thread.Sleep(ReloadSettingsDelay);
                    }
                }
            }
            catch (Exception ex)
            {
                if (logErrors)
                    LogLoadError(
                        plugin,
                        string.Format(
                            Messages.SettingStore_Error_ErrorOnReadingFile,
                            xmlPath,
                            ex.Message),
                        new object[0]);
                
                return null;
            }

            return xmlDocument;
        }

        private void SaveSettings(Plugin plugin, XmlDocument xmlDocument, string xmlPath)
        {
            _loadedSettings[xmlPath] = plugin;

            if (!Directory.Exists(_settingsDirectory))
            {
                Directory.CreateDirectory(_settingsDirectory);
                EnableFileSystemWatcher();
            }

            var settings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "  ",
                Encoding = Encoding.UTF8
            };

            _isSavingSet.Add(xmlPath);

            try
            {
                var timeout = 0;
                
                while (true)
                {
                    try
                    {
                        using (var writer = XmlWriter.Create(xmlPath, settings))
                        {
                            xmlDocument.Save(writer);
                            break;
                        }
                    }
                    catch (Exception)
                    {
                        ++timeout;

                        if (timeout == ReloadSettingsDelay)
                            throw;

                        Thread.Sleep(ReloadSettingsDelay);
                    }
                }
            }
            catch (Exception ex)
            {
                LogSaveError(
                    plugin,
                    string.Format(
                        Messages.SettingStore_Error_ErrorOnWritingFile,
                        xmlPath,
                        ex.Message),
                    new object[0]);
            }
            finally
            {
                _isSavingSet.Remove(xmlPath);
            }
        }

        private OrderedDictionary<ExtensionType, SettingDictionary> ReadPluginSettings(
            Plugin plugin, XmlDocument xmlDocument)
        {
            var orderedDictionary = new OrderedDictionary<ExtensionType, SettingDictionary>();
            var documentElement = xmlDocument.DocumentElement;
            
            Debug.Assert(documentElement != null);
            
            foreach (XmlNode xmlNode in documentElement.ChildNodes)
            {
                if (xmlNode.NodeType != XmlNodeType.Element)
                    continue;

                var attr = ((XmlElement)xmlNode).GetAttribute("name");
                    
                if (attr != plugin.Name)
                {
                    LogLoadError(
                        plugin,
                        "Plugin name does not match to settings plugin name '{0}'",
                        new object[] {attr});

                    return orderedDictionary;
                }
                
                foreach (XmlElement xmlElement in xmlNode.ChildNodes)
                {
                    var attribute = xmlElement.GetAttribute("name");

                    if (!plugin.ExtensionTypes.Contains(attribute))
                    {
                        LogLoadError(
                            plugin,
                            "Plugin does not contain a extension with the name '{0}'",
                            new object[]
                                {
                                    attribute
                                });
                    }
                    else
                    {
                        var settingDictionary = GetSettingDictionary(xmlElement, plugin);
                        orderedDictionary.Add(plugin.ExtensionTypes[attribute], settingDictionary);
                    }
                }
            }

            return orderedDictionary;
        }

        private SettingDictionary GetSettingDictionary(XmlNode xmlNode, Plugin plugin)
        {
            var settingDictionary = new SettingDictionary();
            
            foreach (XmlNode node in xmlNode.ChildNodes)
            {
                if (node.NodeType != XmlNodeType.Element)
                    continue;

                var xmlElement = (XmlElement)node;
                var attribute = xmlElement.GetAttribute("key");
                    
                if (settingDictionary.InternalContains(attribute))
                {
                    LogLoadError(
                        plugin,
                        "Setting dictionary already contains key '{0}'",
                        new object[]
                            {
                                attribute
                            });
                }
                else
                {
                    switch (xmlElement.Name)
                    {
                        case "setting":
                            settingDictionary.InternalSetValue(
                                attribute, NewLineRegex.Replace(xmlElement.GetAttribute("value"), "\n"));
                            break;
                        
                        case "list":
                            settingDictionary.InternalSet(attribute, GetSettingList(xmlElement, plugin));
                            break;
                        
                        case "dictionary":
                            settingDictionary.InternalSet(attribute, GetSettingDictionary(xmlElement, plugin));
                            break;
                    }
                }
            }

            return settingDictionary;
        }

        private SettingList GetSettingList(XmlNode xmlNode, Plugin plugin)
        {
            var settingList = new SettingList();
            
            foreach (var xmlElement in xmlNode.ChildNodes.Cast<XmlNode>().
                Where(node => node.NodeType == XmlNodeType.Element).Cast<XmlElement>())
            {
                switch (xmlElement.Name)
                {
                    case "item":
                        settingList.InternalAddValue(
                            NewLineRegex.Replace(xmlElement.GetAttribute("value"), "\n"));
                        break;
                    case "list":
                        settingList.InternalAddCollection(GetSettingList(xmlElement, plugin));
                        break;
                    case "dictionary":
                        settingList.InternalAddCollection(GetSettingDictionary(xmlElement, plugin));
                        break;
                }
            }

            return settingList;
        }

        private XmlDocument WritePluginSettings(OrderedDictionary<ExtensionType,
            SettingDictionary> pluginSettings, Plugin plugin)
        {
            var xmlDocument = LoadSettings(plugin, GetSettingsPath(plugin), false);
            XmlElement xmlElement;
            
            if (xmlDocument == null)
            {
                xmlDocument = new XmlDocument();
                xmlDocument.AppendChild(xmlDocument.CreateXmlDeclaration("1.0", "utf-8", null));
                xmlElement = xmlDocument.CreateElement(SettingsSubDirectory, NamespaceUri);
                xmlDocument.AppendChild(xmlElement);
            }
            else
                xmlElement = xmlDocument.DocumentElement;

            Debug.Assert(xmlElement != null);
            var written = false;
            
            foreach (var element in
                from XmlNode xmlNode in xmlElement.ChildNodes
                where xmlNode.NodeType == XmlNodeType.Element
                select (XmlElement)xmlNode into element
                where element.GetAttribute("name") == plugin.Name
                select element)
            {
                WritePluginSettings(element, plugin, pluginSettings);
                written = true;
            }

            if (!written)
            {
                var element = CreateElement(xmlElement, "plugin", "name", plugin.Name);
                xmlElement.AppendChild(element);
                WritePluginSettings(element, plugin, pluginSettings);
            }

            return xmlDocument;
        }

        private static void WritePluginSettings(XmlNode pluginNode, Plugin plugin,
            IDictionary<ExtensionType, SettingDictionary> pluginSettings)
        {
            var hashSet = new HashSet<string>();
            
            foreach (XmlNode xmlNode in pluginNode.ChildNodes)
            {
                if (xmlNode.NodeType != XmlNodeType.Element)
                    continue;

                var xmlElement = (XmlElement)xmlNode;
                var attribute = xmlElement.GetAttribute("name");

                if (!plugin.ExtensionTypes.Contains(attribute))
                    continue;

                var key = plugin.ExtensionTypes[attribute];

                if (!pluginSettings.ContainsKey(key))
                    pluginNode.RemoveChild(xmlElement);
                else
                {
                    WriteDictionary(xmlElement, pluginSettings[key]);
                    hashSet.Add(attribute);
                }
            }

            foreach (KeyValuePair<ExtensionType, SettingDictionary> keyValuePair in pluginSettings)
            {
                string name = keyValuePair.Key.Name;
                
                if (!hashSet.Contains(name))
                {
                    XmlElement element = CreateElement(pluginNode, "extension", "name", name);
                    WriteDictionary(pluginNode.AppendChild(element), keyValuePair.Value);
                }
            }
        }

        private static void WriteDictionary(XmlNode dictionaryNode, SettingDictionary settingDicitionary)
        {
            var hashSet = new HashSet<string>();
            var index = 0;
            var childNodes = dictionaryNode.ChildNodes;
            
            while (index < childNodes.Count)
            {
                var xmlNode = childNodes[index];

                if (xmlNode.NodeType != XmlNodeType.Element)
                    ++index;
                else
                {
                    var element = (XmlElement) xmlNode;
                    var attribute = element.GetAttribute("key");

                    if (!settingDicitionary.Contains(attribute))
                        dictionaryNode.RemoveChild(element);
                    else
                    {
                        var settingValue = settingDicitionary[attribute];
                        var elementName = GetElementName(settingValue, "setting");
                        
                        if (element.Name != elementName)
                            element = (XmlElement) dictionaryNode.ReplaceChild(
                                CreateElement(dictionaryNode, elementName, "key", attribute), element);
                        
                        WriteValue(element, settingValue);
                        hashSet.Add(attribute);
                        ++index;
                    }
                }
            }

            foreach (var pair in settingDicitionary)
            {
                var key = pair.Key;

                if (hashSet.Contains(key))
                    continue;

                var settingValue = pair.Value;
                var element = CreateElement(dictionaryNode, GetElementName(settingValue, "setting"), "key", key);
                WriteValue((XmlElement)dictionaryNode.AppendChild(element), settingValue);
            }
        }

        private static void WriteList(XmlNode listNode, SettingList settingList)
        {
            var i = 0;
            var j = 0;
            var childNodes = listNode.ChildNodes;
            
            while (i < childNodes.Count)
            {
                var xmlNode = childNodes[i];

                if (xmlNode.NodeType != XmlNodeType.Element)
                    ++i;
                else
                {
                    var element = (XmlElement) xmlNode;
                    var settingValue = settingList[j];
                    var elementName = GetElementName(settingValue, "item");

                    if (i == settingList.Count || element.Name != elementName ||
                        elementName == "item" && element.GetAttribute("value") != (string) settingValue)
                        listNode.RemoveChild(element);
                    else
                    {
                        WriteValue(element, settingValue);
                        ++j;
                        ++i;
                    }
                }
            }

            for ( ; j < settingList.Count; ++j)
            {
                var settingValue = settingList[j];
                var element = CreateElement(listNode, GetElementName(settingValue, "item"));
                WriteValue((XmlElement)listNode.AppendChild(element), settingValue);
            }
        }

        private static void WriteValue(XmlElement element, object value)
        {
            switch (element.Name)
            {
                case "setting":
                case "item":
                    element.SetAttribute("value", (string)value);
                    break;
                
                case "dictionary":
                    WriteDictionary(element, (SettingDictionary)value);
                    break;
                
                case "list":
                    WriteList(element, (SettingList)value);
                    break;
                
                default:
                    throw new InvalidOperationException("Invalid element name");
            }
        }

        private static string GetElementName(object settingValue, string defaultName)
        {
            if (settingValue is string)
                return defaultName;
            
            if (settingValue is SettingDictionary)
                return "dictionary";
            
            if (settingValue is SettingList)
                return "list";
            
            throw new InvalidOperationException("Invalid setting value");
        }

        private static XmlElement CreateElement(XmlNode parentNode, string elementName)
        {
            Debug.Assert(parentNode.OwnerDocument != null);
            return parentNode.OwnerDocument.CreateElement(elementName, NamespaceUri);
        }

        private static XmlElement CreateElement(XmlNode parentNode, string elementName,
            string attributeKey, string attributeValue)
        {
            var element = CreateElement(parentNode, elementName);
            
            element.SetAttribute(attributeKey, attributeValue);
            
            return element;
        }

        private void FileCreated(object sender, FileSystemEventArgs e)
        {
            new SettingsChangedDelegate(UpdateSettings).BeginInvoke(e.FullPath, null, null);
        }

        private void FileRenamed(object sender, RenamedEventArgs e)
        {
            new SettingsChangedDelegate(ClearSettings).BeginInvoke(e.OldFullPath, null, null);
            new SettingsChangedDelegate(UpdateSettings).BeginInvoke(e.FullPath, null, null);
        }

        private void FileDeleted(object sender, FileSystemEventArgs e)
        {
            new SettingsChangedDelegate(ClearSettings).BeginInvoke(e.FullPath, null, null);
        }

        private void FileChanged(object sender, FileSystemEventArgs e)
        {
            new SettingsChangedDelegate(UpdateSettings).BeginInvoke(e.FullPath, null, null);
        }

        private void UpdateSettings(string settingsPath)
        {
            if (SkipUpdate(settingsPath))
                return;

            var plugin = _loadedSettings[settingsPath];
            var xmlDocument = LoadSettings(plugin, settingsPath, true);
            var orderedDictionary = ReadPluginSettings(plugin, xmlDocument);

            try
            {
                Runtime.Dispatcher.BeginInvoke(
                    new UpdateSettingsDelegate(UpdateSettings), (object)plugin, (object)orderedDictionary);
            }
            catch (ObjectDisposedException)
            { }
        }

        private bool SkipUpdate(string updatePath)
        {
            if (!_loadedSettings.ContainsKey(updatePath) || _isSavingSet.Contains(updatePath))
                return true;

            _updateThread = Thread.CurrentThread;
            Thread.Sleep(ReloadSettingsDelay);
            
            return _updateThread != Thread.CurrentThread;
        }

        private void ClearSettings(string settingsPath)
        {
            if (SkipUpdate(settingsPath))
                return;

            Runtime.Dispatcher.BeginInvoke(
                new ClearSettingsDelegate(ClearSettings),
                new object[] {_loadedSettings[settingsPath]});
        }

        private static void ValidationEventHandler(object sender, ValidationEventArgs e)
        {
            throw e.Exception;
        }

        private string GetSettingsPath(Plugin plugin)
        {
            return Path.Combine(_settingsDirectory, plugin.Name + SettingsFileExtension);
        }

        private void LogLoadError(Plugin plugin, string message, params object[] args)
        {
            Logger.Log(
                new LogArgs(
                    LogType.Error,
                    this,
                    Messages.SettingStore_ErrorOnLoadingSettingsForPlugin,
                    new object[]
                        {
                            plugin.Id,
                            plugin.Name,
                            plugin.OriginUri,
                            string.Format(message, args)
                        }));
        }

        private void LogSaveError(Plugin plugin, string message, params object[] args)
        {
            Logger.Log(
                new LogArgs(
                    LogType.Error,
                    this,
                    Messages.SettingStore_ErrorOnSavingSettingsForPlugin,
                    new object[]
                        {
                            plugin.Id,
                            plugin.Name,
                            plugin.OriginUri,
                            string.Format(message, args)
                        }));
        }

        private void EnableFileSystemWatcher()
        {
            if (_fileSystemWatcher != null)
                return;

            _fileSystemWatcher = new FileSystemWatcher(_settingsDirectory, "*" + SettingsFileExtension);
            
            _fileSystemWatcher.Created += FileCreated;
            _fileSystemWatcher.Changed += FileChanged;
            _fileSystemWatcher.Deleted += FileDeleted;
            _fileSystemWatcher.Renamed += FileRenamed;
            
            _fileSystemWatcher.EnableRaisingEvents = true;
        }

        private void DisableFileSystemWatcher(object sender, EventArgs eventArgs)
        {
            if (_fileSystemWatcher == null)
                return;

            _fileSystemWatcher.Created -= FileCreated;
            _fileSystemWatcher.Changed -= FileChanged;
            _fileSystemWatcher.Deleted -= FileDeleted;
            _fileSystemWatcher.Renamed -= FileRenamed;
            
            _fileSystemWatcher.EnableRaisingEvents = false;
        }

        private delegate void SettingsChangedDelegate(string path);

        private delegate void UpdateSettingsDelegate(
            Plugin plugin, OrderedDictionary<ExtensionType, SettingDictionary> settings);

        private delegate void ClearSettingsDelegate(Plugin plugin);
    }
}
